// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package com.google.protobuf.util;

import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.google.protobuf.Any;
import com.google.protobuf.BoolValue;
import com.google.protobuf.ByteString;
import com.google.protobuf.BytesValue;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.google.protobuf.DoubleValue;
import com.google.protobuf.Duration;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.FieldMask;
import com.google.protobuf.FloatValue;
import com.google.protobuf.Int32Value;
import com.google.protobuf.Int64Value;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.ListValue;
import com.google.protobuf.Message;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.StringValue;
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.UInt32Value;
import com.google.protobuf.UInt64Value;
import com.google.protobuf.Value;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/**
 * Utility classes to convert protobuf messages to/from JSON format. The JSON
 * format follows Proto3 JSON specification and only proto3 features are
 * supported. Proto2 only features (e.g., extensions and unknown fields) will
 * be discarded in the conversion. That is, when converting proto2 messages
 * to JSON format, extensions and unknown fields will be treated as if they
 * do not exist. This applies to proto2 messages embedded in proto3 messages
 * as well.
 */
public class JsonFormat {
  private static final Logger logger =
      Logger.getLogger(JsonFormat.class.getName());

  private JsonFormat() {}
  
  /**
   * Creates a {@link Printer} with default configurations.
   */
  public static Printer printer() {
    return new Printer(TypeRegistry.getEmptyTypeRegistry());
  }
  
  /**
   * A Printer converts protobuf message to JSON format.
   */
  public static class Printer {
    private final TypeRegistry registry;
    
    private Printer(TypeRegistry registry) {
      this.registry = registry;
    }
    
    /**
     * Creates a new {@link Printer} using the given registry. The new Printer
     * clones all other configurations from the current {@link Printer}.
     * 
     * @throws IllegalArgumentException if a registry is already set.
     */
    public Printer usingTypeRegistry(TypeRegistry registry) {
      if (this.registry != TypeRegistry.getEmptyTypeRegistry()) {
        throw new IllegalArgumentException("Only one registry is allowed.");
      }
      return new Printer(registry);
    }
    
    /**
     * Converts a protobuf message to JSON format.
     * 
     * @throws InvalidProtocolBufferException if the message contains Any types
     *         that can't be resolved.
     * @throws IOException if writing to the output fails.
     */
    public void appendTo(MessageOrBuilder message, Appendable output)
        throws IOException {
      // TODO(xiaofeng): Investigate the allocation overhead and optimize for
      // mobile.
      new PrinterImpl(registry, output).print(message);
    }

    /**
     * Converts a protobuf message to JSON format. Throws exceptions if there
     * are unknown Any types in the message. 
     */
    public String print(MessageOrBuilder message)
        throws InvalidProtocolBufferException {
      try {
        StringBuilder builder = new StringBuilder();
        appendTo(message, builder);
        return builder.toString();
      } catch (InvalidProtocolBufferException e) {
        throw e;
      } catch (IOException e) {
        // Unexpected IOException.
        throw new IllegalStateException(e);
      }
    }
  }

  /**
   * Creates a {@link Parser} with default configuration.
   */
  public static Parser parser() {
    return new Parser(TypeRegistry.getEmptyTypeRegistry());
  }
  
  /**
   * A Parser parses JSON to protobuf message.
   */
  public static class Parser {
    private final TypeRegistry registry;
    
    private Parser(TypeRegistry registry) {
      this.registry = registry; 
    }
    
    /**
     * Creates a new {@link Parser} using the given registry. The new Parser
     * clones all other configurations from this Parser.
     * 
     * @throws IllegalArgumentException if a registry is already set.
     */
    public Parser usingTypeRegistry(TypeRegistry registry) {
      if (this.registry != TypeRegistry.getEmptyTypeRegistry()) {
        throw new IllegalArgumentException("Only one registry is allowed.");
      }
      return new Parser(registry);
    }
    
    /**
     * Parses from JSON into a protobuf message.
     * 
     * @throws InvalidProtocolBufferException if the input is not valid JSON
     *         format or there are unknown fields in the input.
     */
    public void merge(String json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      // TODO(xiaofeng): Investigate the allocation overhead and optimize for
      // mobile.
      new ParserImpl(registry).merge(json, builder);
    }
    
    /**
     * Parses from JSON into a protobuf message.
     * 
     * @throws InvalidProtocolBufferException if the input is not valid JSON
     *         format or there are unknown fields in the input.
     * @throws IOException if reading from the input throws.
     */
    public void merge(Reader json, Message.Builder builder)
        throws IOException {
      // TODO(xiaofeng): Investigate the allocation overhead and optimize for
      // mobile.
      new ParserImpl(registry).merge(json, builder);
    }
  }

  /**
   * A TypeRegistry is used to resolve Any messages in the JSON conversion.
   * You must provide a TypeRegistry containing all message types used in
   * Any message fields, or the JSON conversion will fail because data
   * in Any message fields is unrecognizable. You don't need to supply a
   * TypeRegistry if you don't use Any message fields.
   */
  public static class TypeRegistry {
    private static class EmptyTypeRegistryHolder {
      private static final TypeRegistry EMPTY = new TypeRegistry(
          Collections.<String, Descriptor>emptyMap());
    }

    public static TypeRegistry getEmptyTypeRegistry() {
      return EmptyTypeRegistryHolder.EMPTY;
    }

    public static Builder newBuilder() {
      return new Builder();
    }

    /**
     * Find a type by its full name. Returns null if it cannot be found in
     * this {@link TypeRegistry}.
     */
    public Descriptor find(String name) {
      return types.get(name);
    }

    private final Map<String, Descriptor> types;

    private TypeRegistry(Map<String, Descriptor> types) {
      this.types = types;
    }

    /**
     * A Builder is used to build {@link TypeRegistry}.
     */
    public static class Builder {
      private Builder() {}

      /**
       * Adds a message type and all types defined in the same .proto file as
       * well as all transitively imported .proto files to this {@link Builder}.
       */
      public Builder add(Descriptor messageType) {
        if (types == null) {
          throw new IllegalStateException(
              "A TypeRegistry.Builer can only be used once.");
        }
        addFile(messageType.getFile());
        return this;
      }

      /**
       * Adds message types and all types defined in the same .proto file as
       * well as all transitively imported .proto files to this {@link Builder}.
       */
      public Builder add(Iterable<Descriptor> messageTypes) {
        if (types == null) {
          throw new IllegalStateException(
              "A TypeRegistry.Builer can only be used once.");
        }
        for (Descriptor type : messageTypes) {
          addFile(type.getFile());
        }
        return this;
      }

      /**
       * Builds a {@link TypeRegistry}. This method can only be called once for
       * one Builder.
       */
      public TypeRegistry build() {
        TypeRegistry result = new TypeRegistry(types);
        // Make sure the built {@link TypeRegistry} is immutable.
        types = null;
        return result;
      }

      private void addFile(FileDescriptor file) {
        // Skip the file if it's already added.
        if (files.contains(file.getName())) {
          return;
        }
        for (FileDescriptor dependency : file.getDependencies()) {
          addFile(dependency);
        }
        for (Descriptor message : file.getMessageTypes()) {
          addMessage(message);
        }
      }

      private void addMessage(Descriptor message) {
        for (Descriptor nestedType : message.getNestedTypes()) {
          addMessage(nestedType);
        }

        if (types.containsKey(message.getFullName())) {
          logger.warning("Type " + message.getFullName()
              + " is added multiple times.");
          return;
        }

        types.put(message.getFullName(), message);
      }

      private final Set<String> files = new HashSet<String>();
      private Map<String, Descriptor> types =
          new HashMap<String, Descriptor>();
    }
  }

  /**
   * A TextGenerator adds indentation when writing formatted text.
   */
  private static final class TextGenerator {
    private final Appendable output;
    private final StringBuilder indent = new StringBuilder();
    private boolean atStartOfLine = true;

    private TextGenerator(final Appendable output) {
      this.output = output;
    }

    /**
     * Indent text by two spaces.  After calling Indent(), two spaces will be
     * inserted at the beginning of each line of text.  Indent() may be called
     * multiple times to produce deeper indents.
     */
    public void indent() {
      indent.append("  ");
    }

    /**
     * Reduces the current indent level by two spaces, or crashes if the indent
     * level is zero.
     */
    public void outdent() {
      final int length = indent.length();
      if (length < 2) {
        throw new IllegalArgumentException(
            " Outdent() without matching Indent().");
      }
      indent.delete(length - 2, length);
    }

    /**
     * Print text to the output stream.
     */
    public void print(final CharSequence text) throws IOException {
      final int size = text.length();
      int pos = 0;

      for (int i = 0; i < size; i++) {
        if (text.charAt(i) == '\n') {
          write(text.subSequence(pos, i + 1));
          pos = i + 1;
          atStartOfLine = true;
        }
      }
      write(text.subSequence(pos, size));
    }

    private void write(final CharSequence data) throws IOException {
      if (data.length() == 0) {
        return;
      }
      if (atStartOfLine) {
        atStartOfLine = false;
        output.append(indent);
      }
      output.append(data);
    }
  }

  /**
   * A Printer converts protobuf messages to JSON format.
   */
  private static final class PrinterImpl {
    private final TypeRegistry registry;
    private final TextGenerator generator;
    // We use Gson to help handle string escapes.
    private final Gson gson;

    private static class GsonHolder {
      private static final Gson DEFAULT_GSON = new Gson();
    }

    PrinterImpl(TypeRegistry registry, Appendable jsonOutput) {
      this.registry = registry;
      this.generator = new TextGenerator(jsonOutput);
      this.gson = GsonHolder.DEFAULT_GSON;
    }

    void print(MessageOrBuilder message) throws IOException {
      WellKnownTypePrinter specialPrinter = wellKnownTypePrinters.get(
          message.getDescriptorForType().getFullName());
      if (specialPrinter != null) {
        specialPrinter.print(this, message);
        return;
      }
      print(message, null);
    }
    
    private interface WellKnownTypePrinter {
      void print(PrinterImpl printer, MessageOrBuilder message)
          throws IOException;
    }
    
    private static final Map<String, WellKnownTypePrinter>
    wellKnownTypePrinters = buildWellKnownTypePrinters();
    
    private static Map<String, WellKnownTypePrinter>
    buildWellKnownTypePrinters() {
      Map<String, WellKnownTypePrinter> printers =
          new HashMap<String, WellKnownTypePrinter>();
      // Special-case Any.
      printers.put(Any.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printAny(message);
        }
      });
      // Special-case wrapper types.
      WellKnownTypePrinter wrappersPrinter = new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printWrapper(message);
          
        }
      };
      printers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(Int64Value.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(UInt64Value.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(StringValue.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(BytesValue.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter);
      printers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter);
      // Special-case Timestamp.
      printers.put(Timestamp.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printTimestamp(message);
        }
      });
      // Special-case Duration.
      printers.put(Duration.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printDuration(message);
        }
      });
      // Special-case FieldMask.
      printers.put(FieldMask.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printFieldMask(message);
        }
      });
      // Special-case Struct.
      printers.put(Struct.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printStruct(message);
        }
      });
      // Special-case Value.
      printers.put(Value.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printValue(message);
        }
      });
      // Special-case ListValue.
      printers.put(ListValue.getDescriptor().getFullName(),
          new WellKnownTypePrinter() {
        @Override
        public void print(PrinterImpl printer, MessageOrBuilder message)
            throws IOException {
          printer.printListValue(message);
        }
      });
      return printers;
    }
    
    /** Prints google.protobuf.Any */
    private void printAny(MessageOrBuilder message) throws IOException {
      Descriptor descriptor = message.getDescriptorForType();
      FieldDescriptor typeUrlField = descriptor.findFieldByName("type_url");
      FieldDescriptor valueField = descriptor.findFieldByName("value");
      // Validates type of the message. Note that we can't just cast the message
      // to com.google.protobuf.Any because it might be a DynamicMessage. 
      if (typeUrlField == null || valueField == null
          || typeUrlField.getType() != FieldDescriptor.Type.STRING
          || valueField.getType() != FieldDescriptor.Type.BYTES) {
        throw new InvalidProtocolBufferException("Invalid Any type.");
      }
      String typeUrl = (String) message.getField(typeUrlField);
      String typeName = getTypeName(typeUrl);
      Descriptor type = registry.find(typeName);
      if (type == null) {
        throw new InvalidProtocolBufferException(
            "Cannot find type for url: " + typeUrl);
      }
      ByteString content = (ByteString) message.getField(valueField);
      Message contentMessage = DynamicMessage.getDefaultInstance(type)
          .getParserForType().parseFrom(content);
      WellKnownTypePrinter printer = wellKnownTypePrinters.get(typeName);
      if (printer != null) {
        // If the type is one of the well-known types, we use a special
        // formatting.
        generator.print("{\n");
        generator.indent();
        generator.print("\"@type\": " + gson.toJson(typeUrl) + ",\n");
        generator.print("\"value\": ");
        printer.print(this, contentMessage);
        generator.print("\n");
        generator.outdent();
        generator.print("}");
      } else {
        // Print the content message instead (with a "@type" field added).
        print(contentMessage, typeUrl);
      }
    }
    
    /** Prints wrapper types (e.g., google.protobuf.Int32Value) */
    private void printWrapper(MessageOrBuilder message) throws IOException {
      Descriptor descriptor = message.getDescriptorForType();
      FieldDescriptor valueField = descriptor.findFieldByName("value");
      if (valueField == null) {
        throw new InvalidProtocolBufferException("Invalid Wrapper type.");
      }
      // When formatting wrapper types, we just print its value field instead of
      // the whole message.
      printSingleFieldValue(valueField, message.getField(valueField));
    }
    
    private ByteString toByteString(MessageOrBuilder message) {
      if (message instanceof Message) {
        return ((Message) message).toByteString();
      } else {
        return ((Message.Builder) message).build().toByteString();
      }
    }
    
    /** Prints google.protobuf.Timestamp */
    private void printTimestamp(MessageOrBuilder message) throws IOException {
      Timestamp value = Timestamp.parseFrom(toByteString(message));
      generator.print("\"" + TimeUtil.toString(value) + "\"");
    }
    
    /** Prints google.protobuf.Duration */
    private void printDuration(MessageOrBuilder message) throws IOException {
      Duration value = Duration.parseFrom(toByteString(message));
      generator.print("\"" + TimeUtil.toString(value) + "\"");
      
    }
    
    /** Prints google.protobuf.FieldMask */
    private void printFieldMask(MessageOrBuilder message) throws IOException {
      FieldMask value = FieldMask.parseFrom(toByteString(message));
      generator.print("\"" + FieldMaskUtil.toString(value) + "\"");
    }
    
    /** Prints google.protobuf.Struct */
    private void printStruct(MessageOrBuilder message) throws IOException {
      Descriptor descriptor = message.getDescriptorForType();
      FieldDescriptor field = descriptor.findFieldByName("fields");
      if (field == null) {
        throw new InvalidProtocolBufferException("Invalid Struct type.");
      }
      // Struct is formatted as a map object.
      printMapFieldValue(field, message.getField(field));
    }
    
    /** Prints google.protobuf.Value */
    private void printValue(MessageOrBuilder message) throws IOException {
      // For a Value message, only the value of the field is formatted.
      Map<FieldDescriptor, Object> fields = message.getAllFields();
      if (fields.isEmpty()) {
        // No value set.
        generator.print("null");
        return;
      }
      // A Value message can only have at most one field set (it only contains
      // an oneof).
      if (fields.size() != 1) {
        throw new InvalidProtocolBufferException("Invalid Value type.");
      }
      for (Map.Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
        printSingleFieldValue(entry.getKey(), entry.getValue());
      }
    }
    
    /** Prints google.protobuf.ListValue */
    private void printListValue(MessageOrBuilder message) throws IOException {
      Descriptor descriptor = message.getDescriptorForType();
      FieldDescriptor field = descriptor.findFieldByName("values");
      if (field == null) {
        throw new InvalidProtocolBufferException("Invalid ListValue type.");
      }
      printRepeatedFieldValue(field, message.getField(field));
    }

    /** Prints a regular message with an optional type URL. */
    private void print(MessageOrBuilder message, String typeUrl)
        throws IOException {
      generator.print("{\n");
      generator.indent();

      boolean printedField = false;
      if (typeUrl != null) {
        generator.print("\"@type\": " + gson.toJson(typeUrl));
        printedField = true;
      }
      for (Map.Entry<FieldDescriptor, Object> field
          : message.getAllFields().entrySet()) {
        // Skip unknown enum fields.
        if (field.getValue() instanceof EnumValueDescriptor
            && ((EnumValueDescriptor) field.getValue()).getIndex() == -1) {
          continue;
        }
        if (printedField) {
          // Add line-endings for the previous field.
          generator.print(",\n");
        } else {
          printedField = true;
        }
        printField(field.getKey(), field.getValue());
      }
      
      // Add line-endings for the last field.
      if (printedField) {
        generator.print("\n");
      }
      generator.outdent();
      generator.print("}");
    }

    private void printField(FieldDescriptor field, Object value)
        throws IOException {
      generator.print("\"" + fieldNameToCamelName(field.getName()) + "\": ");
      if (field.isMapField()) {
        printMapFieldValue(field, value);
      } else if (field.isRepeated()) {
        printRepeatedFieldValue(field, value);
      } else {
        printSingleFieldValue(field, value);
      }
    }
    
    @SuppressWarnings("rawtypes")
    private void printRepeatedFieldValue(FieldDescriptor field, Object value)
        throws IOException {
      generator.print("[");
      boolean printedElement = false;
      for (Object element : (List) value) {
        // Skip unknown enum entries.
        if (element instanceof EnumValueDescriptor
            && ((EnumValueDescriptor) element).getIndex() == -1) {
          continue;
        }
        if (printedElement) {
          generator.print(", ");
        } else {
          printedElement = true;
        }
        printSingleFieldValue(field, element);
      }
      generator.print("]");
    }
    
    @SuppressWarnings("rawtypes")
    private void printMapFieldValue(FieldDescriptor field, Object value)
        throws IOException {
      Descriptor type = field.getMessageType();
      FieldDescriptor keyField = type.findFieldByName("key");
      FieldDescriptor valueField = type.findFieldByName("value");
      if (keyField == null || valueField == null) {
        throw new InvalidProtocolBufferException("Invalid map field.");
      }
      generator.print("{\n");
      generator.indent();
      boolean printedElement = false;
      for (Object element : (List) value) {
        Message entry = (Message) element;
        Object entryKey = entry.getField(keyField);
        Object entryValue = entry.getField(valueField);
        // Skip unknown enum entries.
        if (entryValue instanceof EnumValueDescriptor
            && ((EnumValueDescriptor) entryValue).getIndex() == -1) {
          continue;
        }
        if (printedElement) {
          generator.print(",\n");
        } else {
          printedElement = true;
        }
        // Key fields are always double-quoted.
        printSingleFieldValue(keyField, entryKey, true);
        generator.print(": ");
        printSingleFieldValue(valueField, entryValue);
      }
      if (printedElement) {
        generator.print("\n");
      }
      generator.outdent();
      generator.print("}");
    }
    
    private void printSingleFieldValue(FieldDescriptor field, Object value)
        throws IOException {
      printSingleFieldValue(field, value, false);
    }

    /**
     * Prints a field's value in JSON format.
     * 
     * @param alwaysWithQuotes whether to always add double-quotes to primitive
     *        types.
     */
    private void printSingleFieldValue(
        final FieldDescriptor field, final Object value,
        boolean alwaysWithQuotes) throws IOException {
      switch (field.getType()) {
        case INT32:
        case SINT32:
        case SFIXED32:
          if (alwaysWithQuotes) {
            generator.print("\"");
          }
          generator.print(((Integer) value).toString());
          if (alwaysWithQuotes) {
            generator.print("\"");
          }
          break;

        case INT64:
        case SINT64:
        case SFIXED64:
          generator.print("\"" + ((Long) value).toString() + "\"");
          break;

        case BOOL:
          if (alwaysWithQuotes) {
            generator.print("\"");
          }
          if (((Boolean) value).booleanValue()) {
            generator.print("true");
          } else {
            generator.print("false");
          }
          if (alwaysWithQuotes) {
            generator.print("\"");
          }
          break;

        case FLOAT:
          Float floatValue = (Float) value;
          if (floatValue.isNaN()) {
            generator.print("\"NaN\"");
          } else if (floatValue.isInfinite()) {
            if (floatValue < 0) {
              generator.print("\"-Infinity\"");
            } else {
              generator.print("\"Infinity\"");
            }
          } else {
            if (alwaysWithQuotes) {
              generator.print("\"");
            }
            generator.print(floatValue.toString());
            if (alwaysWithQuotes) {
              generator.print("\"");
            }
          }
          break;
          
        case DOUBLE:
          Double doubleValue = (Double) value;
          if (doubleValue.isNaN()) {
            generator.print("\"NaN\"");
          } else if (doubleValue.isInfinite()) {
            if (doubleValue < 0) {
              generator.print("\"-Infinity\"");
            } else {
              generator.print("\"Infinity\"");
            }
          } else {
            if (alwaysWithQuotes) {
              generator.print("\"");
            }
            generator.print(doubleValue.toString());
            if (alwaysWithQuotes) {
              generator.print("\"");
            }
          }
          break;

        case UINT32:
        case FIXED32:
          if (alwaysWithQuotes) {
            generator.print("\"");
          }
          generator.print(unsignedToString((Integer) value));
          if (alwaysWithQuotes) {
            generator.print("\"");
          }
          break;

        case UINT64:
        case FIXED64:
          generator.print("\"" + unsignedToString((Long) value) + "\"");
          break;

        case STRING:
          generator.print(gson.toJson(value));
          break;

        case BYTES:
          generator.print("\"");
          generator.print(
              BaseEncoding.base64().encode(((ByteString) value).toByteArray()));
          generator.print("\"");
          break;

        case ENUM:
          // Special-case google.protobuf.NullValue (it's an Enum).
          if (field.getEnumType().getFullName().equals(
                  "google.protobuf.NullValue")) {
            // No matter what value it contains, we always print it as "null".
            if (alwaysWithQuotes) {
              generator.print("\"");
            }
            generator.print("null");
            if (alwaysWithQuotes) {
              generator.print("\"");
            }
          } else {
            generator.print(
                "\"" + ((EnumValueDescriptor) value).getName() + "\"");
          }
          break;

        case MESSAGE:
        case GROUP:
          print((Message) value);
          break;
      }
    }
  }

  /** Convert an unsigned 32-bit integer to a string. */
  private static String unsignedToString(final int value) {
    if (value >= 0) {
      return Integer.toString(value);
    } else {
      return Long.toString(value & 0x00000000FFFFFFFFL);
    }
  }

  /** Convert an unsigned 64-bit integer to a string. */
  private static String unsignedToString(final long value) {
    if (value >= 0) {
      return Long.toString(value);
    } else {
      // Pull off the most-significant bit so that BigInteger doesn't think
      // the number is negative, then set it again using setBit().
      return BigInteger.valueOf(value & Long.MAX_VALUE)
                       .setBit(Long.SIZE - 1).toString();
    }
  }
  
  private static final String TYPE_URL_PREFIX = "type.googleapis.com";
  
  private static String getTypeName(String typeUrl)
      throws InvalidProtocolBufferException {
    String[] parts = typeUrl.split("/");
    if (parts.length != 2 || !parts[0].equals(TYPE_URL_PREFIX)) {
      throw new InvalidProtocolBufferException(
          "Invalid type url found: " + typeUrl);
    }
    return parts[1];
  }

  private static String fieldNameToCamelName(String name) {
    StringBuilder result = new StringBuilder(name.length());
    boolean isNextUpperCase = false;
    for (int i = 0; i < name.length(); i++) {
      Character ch = name.charAt(i);
      if (Character.isLowerCase(ch)) {
        if (isNextUpperCase) {
          result.append(Character.toUpperCase(ch));
        } else {
          result.append(ch);
        }
        isNextUpperCase = false;
      } else if (Character.isUpperCase(ch)) {
        if (i == 0 && !isNextUpperCase) {
          // Force first letter to lower-case unless explicitly told to
          // capitalize it.
          result.append(Character.toLowerCase(ch));
        } else {
          // Capital letters after the first are left as-is.
          result.append(ch);
        }
        isNextUpperCase = false;
      } else if (Character.isDigit(ch)) {
        result.append(ch);
        isNextUpperCase = true;
      } else {
        isNextUpperCase = true;
      }
    }
    return result.toString();
  }
  
  private static class ParserImpl {
    private final TypeRegistry registry;
    private final JsonParser jsonParser;
    
    ParserImpl(TypeRegistry registry) {
      this.registry = registry;
      this.jsonParser = new JsonParser();
    }
    
    void merge(Reader json, Message.Builder builder)
        throws IOException {
      JsonReader reader = new JsonReader(json);
      reader.setLenient(false);
      merge(jsonParser.parse(reader), builder);
    }
    
    void merge(String json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      try {
        JsonReader reader = new JsonReader(new StringReader(json));
        reader.setLenient(false);
        merge(jsonParser.parse(reader), builder);
      } catch (InvalidProtocolBufferException e) {
        throw e;
      } catch (Exception e) {
        // We convert all exceptions from JSON parsing to our own exceptions.
        throw new InvalidProtocolBufferException(e.getMessage());
      }
    }
    
    private interface WellKnownTypeParser {
      void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
          throws InvalidProtocolBufferException;
    }
    
    private static final Map<String, WellKnownTypeParser> wellKnownTypeParsers =
        buildWellKnownTypeParsers();
    
    private static Map<String, WellKnownTypeParser>
    buildWellKnownTypeParsers() {
      Map<String, WellKnownTypeParser> parsers =
          new HashMap<String, WellKnownTypeParser>();
      // Special-case Any.
      parsers.put(Any.getDescriptor().getFullName(), new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeAny(json, builder);
        }
      });
      // Special-case wrapper types.
      WellKnownTypeParser wrappersPrinter = new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeWrapper(json, builder);
        }
      };
      parsers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(Int64Value.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(UInt64Value.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(StringValue.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(BytesValue.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter);
      parsers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter);
      // Special-case Timestamp.
      parsers.put(Timestamp.getDescriptor().getFullName(),
          new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeTimestamp(json, builder);
        }
      });
      // Special-case Duration.
      parsers.put(Duration.getDescriptor().getFullName(),
          new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeDuration(json, builder);
        }
      });
      // Special-case FieldMask.
      parsers.put(FieldMask.getDescriptor().getFullName(),
          new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeFieldMask(json, builder);
        }
      });
      // Special-case Struct.
      parsers.put(Struct.getDescriptor().getFullName(),
          new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeStruct(json, builder);
        }
      });
      // Special-case Value.
      parsers.put(Value.getDescriptor().getFullName(),
          new WellKnownTypeParser() {
        @Override
        public void merge(ParserImpl parser, JsonElement json,
            Message.Builder builder) throws InvalidProtocolBufferException {
          parser.mergeValue(json, builder);
        }
      });
      return parsers;
    }
    
    private void merge(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      WellKnownTypeParser specialParser = wellKnownTypeParsers.get(
          builder.getDescriptorForType().getFullName());
      if (specialParser != null) {
        specialParser.merge(this, json, builder);
        return;
      }
      mergeMessage(json, builder, false);
    }
    
    // Maps from camel-case field names to FieldDescriptor.
    private final Map<Descriptor, Map<String, FieldDescriptor>> fieldNameMaps =
        new HashMap<Descriptor, Map<String, FieldDescriptor>>();
    
    private Map<String, FieldDescriptor> getFieldNameMap(
        Descriptor descriptor) {
      if (!fieldNameMaps.containsKey(descriptor)) {
        Map<String, FieldDescriptor> fieldNameMap =
            new HashMap<String, FieldDescriptor>();
        for (FieldDescriptor field : descriptor.getFields()) {
          fieldNameMap.put(fieldNameToCamelName(field.getName()), field);
        }
        fieldNameMaps.put(descriptor, fieldNameMap);
        return fieldNameMap;
      }
      return fieldNameMaps.get(descriptor);
    }
    
    private void mergeMessage(JsonElement json, Message.Builder builder,
        boolean skipTypeUrl) throws InvalidProtocolBufferException {
      if (!(json instanceof JsonObject)) {
        throw new InvalidProtocolBufferException(
            "Expect message object but got: " + json);
      }
      JsonObject object = (JsonObject) json;
      Map<String, FieldDescriptor> fieldNameMap =
          getFieldNameMap(builder.getDescriptorForType());
      for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
        if (skipTypeUrl && entry.getKey().equals("@type")) {
          continue;
        }
        FieldDescriptor field = fieldNameMap.get(entry.getKey());
        if (field == null) {
          throw new InvalidProtocolBufferException(
              "Cannot find field: " + entry.getKey() + " in message "
              + builder.getDescriptorForType().getFullName());
        }
        mergeField(field, entry.getValue(), builder);
      }
    }
    
    private void mergeAny(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      Descriptor descriptor = builder.getDescriptorForType();
      FieldDescriptor typeUrlField = descriptor.findFieldByName("type_url");
      FieldDescriptor valueField = descriptor.findFieldByName("value");
      // Validates type of the message. Note that we can't just cast the message
      // to com.google.protobuf.Any because it might be a DynamicMessage. 
      if (typeUrlField == null || valueField == null
          || typeUrlField.getType() != FieldDescriptor.Type.STRING
          || valueField.getType() != FieldDescriptor.Type.BYTES) {
        throw new InvalidProtocolBufferException("Invalid Any type.");
      }
      
      if (!(json instanceof JsonObject)) {
        throw new InvalidProtocolBufferException(
            "Expect message object but got: " + json);
      }
      JsonObject object = (JsonObject) json;
      JsonElement typeUrlElement = object.get("@type");
      if (typeUrlElement == null) {
        throw new InvalidProtocolBufferException(
            "Missing type url when parsing: " + json);
      }
      String typeUrl = typeUrlElement.getAsString();
      Descriptor contentType = registry.find(getTypeName(typeUrl));
      if (contentType == null) {
        throw new InvalidProtocolBufferException(
            "Cannot resolve type: " + typeUrl);
      }
      builder.setField(typeUrlField, typeUrl);
      Message.Builder contentBuilder =
          DynamicMessage.getDefaultInstance(contentType).newBuilderForType();
      WellKnownTypeParser specialParser =
          wellKnownTypeParsers.get(contentType.getFullName());
      if (specialParser != null) {
        JsonElement value = object.get("value");
        if (value != null) {
          specialParser.merge(this, value, contentBuilder);
        }
      } else {
        mergeMessage(json, contentBuilder, true);
      }
      builder.setField(valueField, contentBuilder.build().toByteString());
    }
    
    private void mergeFieldMask(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      FieldMask value = FieldMaskUtil.fromString(json.getAsString());
      builder.mergeFrom(value.toByteString());
    }
    
    private void mergeTimestamp(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      try {
        Timestamp value = TimeUtil.parseTimestamp(json.getAsString());
        builder.mergeFrom(value.toByteString());
      } catch (ParseException e) {
        throw new InvalidProtocolBufferException(
            "Failed to parse timestamp: " + json);
      }
    }
    
    private void mergeDuration(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      try {
        Duration value = TimeUtil.parseDuration(json.getAsString());
        builder.mergeFrom(value.toByteString());
      } catch (ParseException e) {
        throw new InvalidProtocolBufferException(
            "Failed to parse duration: " + json);
      }
    }
    
    private void mergeStruct(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      Descriptor descriptor = builder.getDescriptorForType();
      FieldDescriptor field = descriptor.findFieldByName("fields");
      if (field == null) {
        throw new InvalidProtocolBufferException("Invalid Struct type.");
      }
      mergeMapField(field, json, builder);
    }
    
    private void mergeValue(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      Descriptor type = builder.getDescriptorForType();
      if (json instanceof JsonPrimitive) {
        JsonPrimitive primitive = (JsonPrimitive) json;
        if (primitive.isBoolean()) {
          builder.setField(type.findFieldByName("bool_value"),
              primitive.getAsBoolean());
        } else if (primitive.isNumber()) {
          builder.setField(type.findFieldByName("number_value"),
              primitive.getAsDouble());
        } else {
          builder.setField(type.findFieldByName("string_value"),
              primitive.getAsString());
        }
      } else if (json instanceof JsonObject) {
        FieldDescriptor field = type.findFieldByName("struct_value");
        Message.Builder structBuilder = builder.newBuilderForField(field);
        merge(json, structBuilder);
        builder.setField(field, structBuilder.build());
      } else if (json instanceof JsonArray) {
        FieldDescriptor field = type.findFieldByName("list_value");
        Message.Builder listBuilder = builder.newBuilderForField(field);
        FieldDescriptor listField =
            listBuilder.getDescriptorForType().findFieldByName("values");
        mergeRepeatedField(listField, json, listBuilder);
        builder.setField(field, listBuilder.build());
      } else {
        throw new IllegalStateException("Unexpected json data: " + json);
      }
    }
    
    private void mergeWrapper(JsonElement json, Message.Builder builder)
        throws InvalidProtocolBufferException {
      Descriptor type = builder.getDescriptorForType();
      FieldDescriptor field = type.findFieldByName("value");
      if (field == null) {
        throw new InvalidProtocolBufferException(
            "Invalid wrapper type: " + type.getFullName());
      }
      builder.setField(field, parseFieldValue(field, json, builder));
    }
    
    private void mergeField(FieldDescriptor field, JsonElement json,
        Message.Builder builder) throws InvalidProtocolBufferException {
      if (json instanceof JsonNull) {
        // We allow "null" as value for all field types and treat it as if the
        // field is not present.
        return;
      }
      if (field.isMapField()) {
        mergeMapField(field, json, builder);
      } else if (field.isRepeated()) {
        mergeRepeatedField(field, json, builder);
      } else {
        Object value = parseFieldValue(field, json, builder);
        if (value != null) {
          builder.setField(field, value);
        }
      }
    }
    
    private void mergeMapField(FieldDescriptor field, JsonElement json,
        Message.Builder builder) throws InvalidProtocolBufferException {
      if (!(json instanceof JsonObject)) {
        throw new InvalidProtocolBufferException(
            "Expect a map object but found: " + json);
      }
      Descriptor type = field.getMessageType();
      FieldDescriptor keyField = type.findFieldByName("key");
      FieldDescriptor valueField = type.findFieldByName("value");
      if (keyField == null || valueField == null) {
        throw new InvalidProtocolBufferException(
            "Invalid map field: " + field.getFullName());
      }
      JsonObject object = (JsonObject) json;
      for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
        Message.Builder entryBuilder = builder.newBuilderForField(field);
        Object key = parseFieldValue(
            keyField, new JsonPrimitive(entry.getKey()), entryBuilder);
        Object value = parseFieldValue(
            valueField, entry.getValue(), entryBuilder);
        if (value == null) {
          value = getDefaultValue(valueField, entryBuilder);
        }
        entryBuilder.setField(keyField, key);
        entryBuilder.setField(valueField, value);
        builder.addRepeatedField(field, entryBuilder.build());
      }
    }
    
    /**
     * Gets the default value for a field type. Note that we use proto3
     * language defaults and ignore any default values set through the
     * proto "default" option. 
     */
    private Object getDefaultValue(FieldDescriptor field,
        Message.Builder builder) {
      switch (field.getType()) {
        case INT32:
        case SINT32:
        case SFIXED32:
        case UINT32:
        case FIXED32:
          return 0;
        case INT64:
        case SINT64:
        case SFIXED64:
        case UINT64:
        case FIXED64:
          return 0L;
        case FLOAT:
          return 0.0f;
        case DOUBLE:
          return 0.0;
        case BOOL:
          return false;
        case STRING:
          return "";
        case BYTES:
          return ByteString.EMPTY;
        case ENUM:
          return field.getEnumType().getValues().get(0);
        case MESSAGE:
        case GROUP:
          return builder.newBuilderForField(field).getDefaultInstanceForType();
        default:
          throw new IllegalStateException(
              "Invalid field type: " + field.getType());
      }
    }
    
    private void mergeRepeatedField(FieldDescriptor field, JsonElement json,
        Message.Builder builder) throws InvalidProtocolBufferException {
      if (!(json instanceof JsonArray)) {
        throw new InvalidProtocolBufferException(
            "Expect an array but found: " + json);
      }
      JsonArray array = (JsonArray) json;
      for (int i = 0; i < array.size(); ++i) {
        Object value = parseFieldValue(field, array.get(i), builder);
        if (value == null) {
          value = getDefaultValue(field, builder);
        }
        builder.addRepeatedField(field, value);
      }
    }
    
    private int parseInt32(JsonElement json)
        throws InvalidProtocolBufferException {
      try {
        return Integer.parseInt(json.getAsString());
      } catch (Exception e) {
        throw new InvalidProtocolBufferException("Not an int32 value: " + json);
      }
    }
    
    private long parseInt64(JsonElement json)
        throws InvalidProtocolBufferException {
      try {
        return Long.parseLong(json.getAsString());
      } catch (Exception e) {
        throw new InvalidProtocolBufferException("Not an int64 value: " + json);
      }
    }
    
    private int parseUint32(JsonElement json)
        throws InvalidProtocolBufferException {
      try {
        long result = Long.parseLong(json.getAsString());
        if (result < 0 || result > 0xFFFFFFFFL) {
          throw new InvalidProtocolBufferException(
              "Out of range uint32 value: " + json);
        }
        return (int) result;
      } catch (InvalidProtocolBufferException e) {
        throw e;
      } catch (Exception e) {
        throw new InvalidProtocolBufferException(
            "Not an uint32 value: " + json);
      }
    }
    
    private static final BigInteger MAX_UINT64 =
        new BigInteger("FFFFFFFFFFFFFFFF", 16);
    
    private long parseUint64(JsonElement json)
        throws InvalidProtocolBufferException {
      try {
        BigInteger value = new BigInteger(json.getAsString());
        if (value.compareTo(BigInteger.ZERO) < 0
            || value.compareTo(MAX_UINT64) > 0) {
          throw new InvalidProtocolBufferException(
              "Out of range uint64 value: " + json);
        }
        return value.longValue();
      } catch (InvalidProtocolBufferException e) {
        throw e;
      } catch (Exception e) {
        throw new InvalidProtocolBufferException(
            "Not an uint64 value: " + json);
      }
    }
    
    private boolean parseBool(JsonElement json)
        throws InvalidProtocolBufferException {
      if (json.getAsString().equals("true")) {
        return true;
      }
      if (json.getAsString().equals("false")) {
        return false;
      }
      throw new InvalidProtocolBufferException("Invalid bool value: " + json);
    }
    
    private static final double EPSILON = 1e-6;
    
    private float parseFloat(JsonElement json)
        throws InvalidProtocolBufferException {
      if (json.getAsString().equals("NaN")) {
        return Float.NaN;
      } else if (json.getAsString().equals("Infinity")) {
        return Float.POSITIVE_INFINITY;
      } else if (json.getAsString().equals("-Infinity")) {
        return Float.NEGATIVE_INFINITY;
      }
      try {
        // We don't use Float.parseFloat() here because that function simply
        // accepts all double values. Here we parse the value into a Double
        // and do explicit range check on it.
        double value = Double.parseDouble(json.getAsString());
        // When a float value is printed, the printed value might be a little
        // larger or smaller due to precision loss. Here we need to add a bit
        // of tolerance when checking whether the float value is in range.
        if (value > Float.MAX_VALUE * (1.0 + EPSILON)
            || value < -Float.MAX_VALUE * (1.0 + EPSILON)) {
          throw new InvalidProtocolBufferException(
              "Out of range float value: " + json);
        }
        return (float) value;
      } catch (InvalidProtocolBufferException e) {
        throw e;
      } catch (Exception e) {
        throw new InvalidProtocolBufferException("Not a float value: " + json);
      }
    }
    
    private static final BigDecimal MORE_THAN_ONE = new BigDecimal(
        String.valueOf(1.0 + EPSILON));
    // When a float value is printed, the printed value might be a little
    // larger or smaller due to precision loss. Here we need to add a bit
    // of tolerance when checking whether the float value is in range.
    private static final BigDecimal MAX_DOUBLE = new BigDecimal(
        String.valueOf(Double.MAX_VALUE)).multiply(MORE_THAN_ONE);
    private static final BigDecimal MIN_DOUBLE = new BigDecimal(
        String.valueOf(-Double.MAX_VALUE)).multiply(MORE_THAN_ONE);
    
    private double parseDouble(JsonElement json)
        throws InvalidProtocolBufferException {
      if (json.getAsString().equals("NaN")) {
        return Double.NaN;
      } else if (json.getAsString().equals("Infinity")) {
        return Double.POSITIVE_INFINITY;
      } else if (json.getAsString().equals("-Infinity")) {
        return Double.NEGATIVE_INFINITY;
      }
      try {
        // We don't use Double.parseDouble() here because that function simply
        // accepts all values. Here we parse the value into a BigDecimal and do
        // explicit range check on it.
        BigDecimal value = new BigDecimal(json.getAsString());
        if (value.compareTo(MAX_DOUBLE) > 0
            || value.compareTo(MIN_DOUBLE) < 0) {
          throw new InvalidProtocolBufferException(
              "Out of range double value: " + json);
        }
        return value.doubleValue();
      } catch (InvalidProtocolBufferException e) {
        throw e;
      } catch (Exception e) {
        throw new InvalidProtocolBufferException(
            "Not an double value: " + json);
      }
    }
    
    private String parseString(JsonElement json) {
      return json.getAsString();
    }
    
    private ByteString parseBytes(JsonElement json) {
      return ByteString.copyFrom(
          BaseEncoding.base64().decode(json.getAsString()));
    }
    
    private EnumValueDescriptor parseEnum(EnumDescriptor enumDescriptor,
        JsonElement json) throws InvalidProtocolBufferException {
      String value = json.getAsString();
      EnumValueDescriptor result = enumDescriptor.findValueByName(value);
      if (result == null) {
        throw new InvalidProtocolBufferException(
            "Invalid enum value: " + value + " for enum type: "
            + enumDescriptor.getFullName());
      }
      return result;
    }
    
    private Object parseFieldValue(FieldDescriptor field, JsonElement json,
        Message.Builder builder) throws InvalidProtocolBufferException {
      if (json instanceof JsonNull) {
        if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE
            && field.getMessageType().getFullName().equals(
                   Value.getDescriptor().getFullName())) {
          // For every other type, "null" means absence, but for the special
          // Value message, it means the "null_value" field has been set.
          Value value = Value.newBuilder().setNullValueValue(0).build();
          return builder.newBuilderForField(field).mergeFrom(
              value.toByteString()).build();
        }
        return null;
      }
      switch (field.getType()) {
        case INT32:
        case SINT32:
        case SFIXED32:
          return parseInt32(json);

        case INT64:
        case SINT64:
        case SFIXED64:
          return parseInt64(json);

        case BOOL:
          return parseBool(json);

        case FLOAT:
          return parseFloat(json);
          
        case DOUBLE:
          return parseDouble(json);

        case UINT32:
        case FIXED32:
          return parseUint32(json);

        case UINT64:
        case FIXED64:
          return parseUint64(json);

        case STRING:
          return parseString(json);

        case BYTES:
          return parseBytes(json);

        case ENUM:
          return parseEnum(field.getEnumType(), json);

        case MESSAGE:
        case GROUP:
          Message.Builder subBuilder = builder.newBuilderForField(field);
          merge(json, subBuilder);
          return subBuilder.build();
          
        default:
          throw new InvalidProtocolBufferException(
              "Invalid field type: " + field.getType());
      } 
    }
  }
}
